﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;

namespace TestRunner 
{
    public class RunCompletedEventArgs:EventArgs
    {
        public TestEntry TestEntry { get; set; }
        public string LogFilePath { get; set; }
        public string ResultFilePath { get; set; }
        public int ExitCode { get; set; }
        public int RunNumber { get; set; }
        public string ErrorMessage { get; set; }
    }

    public class RunStartedEventArgs:EventArgs
    {
        public TestEntry TestEntry { get; set; }
    }

    class TestRunner 
    {
        internal TestRunner(string nunitExecutablePath)
        {
            this.nunitExecutablePath = nunitExecutablePath;
        }

        private readonly string nunitExecutablePath;
        
        private bool stopExecution;

        public event EventHandler<RunStartedEventArgs> TestStarted;
        public event EventHandler<RunCompletedEventArgs> TestCompleted;
        public event EventHandler TestsCompleted;
        private List<TestEntry> tests;
        private int parallelThreadsCount;
        private int testRepeatCount;
        private bool showNunitConsoleOutput;
        public void Stop()
        {
            stopExecution = true;
        }

        public void RunTestAsync(List<TestEntry> testList, int parallesimCount, int repeatCount, TextWriter outputStream, bool showNunitOutput, bool collateTestRuns)
        {
            showNunitConsoleOutput = showNunitOutput;
            collateTests = collateTestRuns;

            outputWriterStream = outputStream;
            outputStream.Flush();

            if (parallesimCount != 1)
            {
                showNunitConsoleOutput = false;
                outputStream.WriteLine("Output is not available when tests are running in parallel.");
            }

            if(testList.Count == 0) return;
            tests = testList;
            parallelThreadsCount = parallesimCount;
            testRepeatCount = repeatCount;
            (new Thread(RunTests)).Start();
        }

        private bool collateTests;
        readonly Queue<TestEntry> independentTestQueue = new Queue<TestEntry>();
        private TextWriter outputWriterStream;
        void RunTests()
        {
            string path = Path.GetDirectoryName(tests[0].Assembly);
            Trace.Assert(path != null);
            Environment.CurrentDirectory = path;
            
            if (!Directory.Exists("Logs")) Directory.CreateDirectory("Logs");
            List<Thread> threads = new List<Thread>();
            List<TestEntry> sequentialTests = tests;

            if (parallelThreadsCount >1)
            {
                var independentTests = tests.Where(p => p.CanRunIndependently).ToList();
                sequentialTests = tests.Where(p => p.CanRunIndependently == false).ToList();

                foreach (var independentTest in independentTests)
                {
                    independentTestQueue.Enqueue(independentTest);
                }
                
                if (independentTestQueue.Count > 0)
                {
                    int threadC = Math.Min(Environment.ProcessorCount, parallelThreadsCount);
                    threadC = Math.Min(threadC, independentTests.Count);

                    if (sequentialTests.Count >= 1 && threadC >1)
                    {
                        threadC--;
                    }

                    for (int i = 0; i < threadC; i++)
                    {
                        var thread = new Thread(RunIndependentTests);
                        thread.IsBackground = true;
                        threads.Add(thread);
                        thread.Start();
                    }
                }
            }

            if (collateTests || testRepeatCount == 1)
            {
                foreach (var testEntry in sequentialTests)
                {
                    if (stopExecution) break;
                    RunTest(testEntry);
                }
            }
            else
            {
                for (int i = 1; i <= testRepeatCount; i++) 
                {
                    foreach (var test in sequentialTests)
                    {
                        if (stopExecution) break;
                        string output = Path.Combine(Environment.CurrentDirectory, "Logs", "Log_" + i + "_" + test.FullName + ".txt");
                        string result = Path.Combine(Environment.CurrentDirectory, "Logs", "Result_" + i + "_" + test.FullName + ".xml");
                        RemoveFile(output);
                        RemoveFile(result);

                        RunTest(test, output, result, i);
                    }
                }
            }

            if (parallelThreadsCount >1)
            {
                foreach (var thread in threads)
                {
                    thread.Join();
                }
            }

            var handler = TestsCompleted;
            if (handler != null)
            {
                handler(this, EventArgs.Empty);
            }
        }

        void RunIndependentTests()
        {
            while (true)
            {
                if (stopExecution) return;
                TestEntry test = null;
                lock (independentTestQueue)
                {
                    if (independentTestQueue.Count > 0)
                    {
                        test = independentTestQueue.Dequeue();
                    }
                }
                if (test == null) return;
                if (stopExecution) return;
                RunTest(test);
            }
        }

        void RunTest(TestEntry test)
        {
            if(testRepeatCount == 1)
            {
                string output = Path.Combine(Environment.CurrentDirectory, "Logs", "Log_" + test.FullName + ".txt");
                string result = Path.Combine(Environment.CurrentDirectory, "Logs", "Result_" + test.FullName + ".xml");
                RemoveFile(output);
                RemoveFile(result);

                RunTest(test, output, result);
            } 
            else if (testRepeatCount > 1)
            {
                for (int i = 1; i <= testRepeatCount; i++)
                {
                    if (stopExecution) return;
                    string output = Path.Combine(Environment.CurrentDirectory, "Logs", "Log_" +i +"_" + test.FullName + ".txt");
                    string result = Path.Combine(Environment.CurrentDirectory, "Logs", "Result_" + i + "_" + test.FullName + ".xml");
                    RemoveFile(output);
                    RemoveFile(result);

                    RunTest(test, output, result, i);
                }
            }
        }

        void RemoveFile(string filePath)
        {
            if (File.Exists(filePath))
            {
                int retry = 3;
                do
                {
                    try
                    {
                        File.Delete(filePath);
                    }
                    catch (Exception)
                    {
                        Thread.Sleep(3000);
                    }

                    retry--;
                } while (File.Exists(filePath) && retry >0);
            }
        }

        private void RunTest(TestEntry test, string output, string result, int runNumber = 0)
        {
            NotifyTestStart(test);

            var args = new RunCompletedEventArgs {TestEntry = test};
            args.LogFilePath = output;
            args.ResultFilePath = result;

            args.RunNumber = runNumber;

            try
            {
                int exit = RunTest(test.Assembly, test.FullName, output, result);
                args.ExitCode = exit;
            }
            catch (Exception exception)
            {
                args.ErrorMessage = exception.Message;
            }
            
            NotifyTestCompletion(args);
        }

        private int RunTest(string assemblyName, string test, string logPath, string resultPath)
        {
            bool RedirectOutput = (outputWriterStream != null && showNunitConsoleOutput);
            var processStartInfo = new ProcessStartInfo(nunitExecutablePath);

            processStartInfo.WorkingDirectory = Environment.CurrentDirectory;

            processStartInfo.Arguments = PrepareArguments(assemblyName, test, RedirectOutput? null: logPath, resultPath);
            var process = new Process { StartInfo = processStartInfo };

            processStartInfo.UseShellExecute = false;
            processStartInfo.CreateNoWindow = true;
            processStartInfo.WindowStyle = ProcessWindowStyle.Hidden;
            StringBuilder lines = new StringBuilder();
            if (RedirectOutput)
            {
                //The below lines of code re-direct the output of the started process to the
                //current process's console. This is to ensure that we don't miss the logs/traces
                //of the started process.
                
                processStartInfo.RedirectStandardOutput = true;
                processStartInfo.RedirectStandardError = true;

                //Delegate/Method which will be called when the child process writes something to
                //it's console window. We will intercept that and write to 
                //the current process's console
                DataReceivedEventHandler dataReceived = (sender, e) =>
                    {
                        lines.AppendLine(e.Data);
                        outputWriterStream.WriteLine(e.Data);
                    };

                process.OutputDataReceived += dataReceived;
                process.ErrorDataReceived += dataReceived;
            }
            else if(outputWriterStream != null)
            {
                string msg = String.Format("Starting test {0}", test);
                outputWriterStream.WriteLine(msg);
            }
            
            process.Start();
            if (RedirectOutput)
            {
                process.BeginOutputReadLine();
                process.BeginErrorReadLine();
            }

            process.WaitForExit();
            if (RedirectOutput)
            {
                File.WriteAllText(logPath, lines.ToString());
            }
            return process.ExitCode;
        }

        private static string PrepareArguments(string assemblyName, string test, string logPath, string resultPath)
        {
            string output = logPath;
            string result = resultPath;
            if (logPath != null) 
            {
                return String.Format(@"{0} /noshadow /nothread /nodots /labels /trace=Verbose /nologo /run={1} /result={2} /output={3}", assemblyName, test, result, output);
            }
            return String.Format(@"{0} /noshadow /nothread /nodots /labels /trace=Verbose /nologo /run={1} /result={2}", assemblyName, test, result);
        }

        private void NotifyTestCompletion(RunCompletedEventArgs args)
        {
            var handler = TestCompleted;
            if (handler != null)
            {
                handler(this, args);
            }
        }

        private void NotifyTestStart(TestEntry test)
        {
            var handler = TestStarted;
            if (handler != null)
            {
                handler(this, new RunStartedEventArgs {TestEntry = test});
            }
        }
    }
}
